﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="KinectSensorManager.cs" company="arvystate.net">
//   arvystate.net
// </copyright>
// <summary>
//   Provides Kinect Sensor Wrapper For Presentation
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace PresentationAid.Core
{
    using System;
    using System.Linq;

    using Kinect.Toolbox;

    using Microsoft.Kinect;

    using NLog;

    using PresentationAid.Lib.GestureControllers;
    using PresentationAid.Lib.GestureEngines;
    using PresentationAid.Lib.Gestures;
    using PresentationAid.Settings;

    #region Delegates

    /// <summary>
    /// The skeleton frame ready.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="args">
    /// The args.
    /// </param>
    public delegate void SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs args);

    /// <summary>
    /// The depth frame ready.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="args">
    /// The args.
    /// </param>
    public delegate void DepthFrameReady(object sender, DepthImageFrameReadyEventArgs args);

    /// <summary>
    /// The color frame ready.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="args">
    /// The args.
    /// </param>
    public delegate void ColorFrameReady(object sender, ColorImageFrameReadyEventArgs args);

    /// <summary>
    /// The kinect sensor active changed.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="args">
    /// The args.
    /// </param>
    public delegate void KinectSensorActiveChanged(object sender, KinectSensorEventArgs args);

    /// <summary>
    /// The kinect sensor active started.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="args">
    /// The args.
    /// </param>
    public delegate void KinectSensorActiveStarted(object sender, KinectSensorEventArgs args);

    /// <summary>
    /// The kinect sensor active closed.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="args">
    /// The args.
    /// </param>
    public delegate void KinectSensorActiveClosed(object sender, KinectSensorEventArgs args);

    #endregion

    /// <summary>
    /// Provides Kinect Sensor Wrapper For Presentation
    /// </summary>
    public class KinectSensorManager : IDisposable
    {
        #region Static Fields

        /// <summary>
        /// The logger.
        /// </summary>
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        /// <summary>
        /// Singleton private instance
        /// </summary>
        private static KinectSensorManager instance;

        #endregion

        #region Fields

        /// <summary>
        /// The gesture controller.
        /// </summary>
        private IGestureController gestureController;

        /// <summary>
        /// The kinect sensor.
        /// </summary>
        private KinectSensor kinectSensor;

        /// <summary>
        /// The nui camera.
        /// </summary>
        private BindableNUICamera nuiCamera;

        /// <summary>
        /// The skeletons.
        /// </summary>
        private Skeleton[] skeletons;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Prevents a default instance of the <see cref="KinectSensorManager"/> class from being created. 
        /// Private constructor for Singleton
        /// </summary>
        private KinectSensorManager()
        {
            Logger.Trace("Init: KinectSensorManager");

            KinectSensor.KinectSensors.StatusChanged += this.KinectSensorsOnStatusChanged;

            // Setup default engines and controllers
            switch (SettingManager.Instance.GestureEngineType)
            {
                case 1:
                    this.GestureEngine = new ExtendedKeystrokeEngine();

                    break;
                case 2:
                    this.GestureEngine = new KeystrokeEngine();

                    break;
                default:
                    this.GestureEngine = new PowerPointEngine();

                    break;
            }

            this.GestureEngine.ParseMapping(SettingManager.Instance.GestureEngineMapping);

            Logger.Trace("Init: DefaultMultiplexingGestureController - initialized");

            MultiplexingGestureController multiplexingGestureController = new MultiplexingGestureController();
            multiplexingGestureController.AddGestureController(new MicrosoftGestureController());
            multiplexingGestureController.AddGestureController(new PostureGestureController());

            // multiplexingGestureController.AddGestureController (new FizbinGestureController ());
            this.GestureController = multiplexingGestureController;
        }

        #endregion

        #region Public Events

        /// <summary>
        /// The color frame ready.
        /// </summary>
        public event ColorFrameReady ColorFrameReady;

        /// <summary>
        /// The depth frame ready.
        /// </summary>
        public event DepthFrameReady DepthFrameReady;

        /// <summary>
        /// The gesture recognized.
        /// </summary>
        public event GestureEventHandler GestureRecognized;

        /// <summary>
        /// The kinect sensor active changed.
        /// </summary>
        public event KinectSensorActiveChanged KinectSensorActiveChanged;

        /// <summary>
        /// The kinect sensor active closed.
        /// </summary>
        public event KinectSensorActiveClosed KinectSensorActiveClosed;

        /// <summary>
        /// The kinect sensor active started.
        /// </summary>
        public event KinectSensorActiveStarted KinectSensorActiveStarted;

        /// <summary>
        /// The skeleton frame ready.
        /// </summary>
        public event SkeletonFrameReady SkeletonFrameReady;

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets singleton public access
        /// </summary>
        public static KinectSensorManager Instance
        {
            get
            {
                return instance ?? (instance = new KinectSensorManager());
            }
        }

        /// <summary>
        /// Gets or sets Kinect elevation angle
        /// </summary>
        public int ElevationAngle
        {
            get
            {
                return this.nuiCamera.ElevationAngle;
            }

            set
            {
                if (this.nuiCamera == null)
                {
                    throw new NullReferenceException("Kinect sensor must be started before you can change elevation.");
                }

                this.nuiCamera.ElevationAngle = value;
            }
        }

        /// <summary>
        /// Gets or sets the gesture controller.
        /// </summary>
        /// <exception cref="ArgumentNullException">
        /// Throws exception if is set to null.
        /// </exception>
        public IGestureController GestureController
        {
            get
            {
                return this.gestureController;
            }

            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }

                if (this.gestureController == value)
                {
                    return;
                }

                this.gestureController = value;

                this.gestureController.GestureRecognized += this.OnGestureRecognized;
            }
        }

        /// <summary>
        /// Gets or sets the gesture engine.
        /// </summary>
        public GestureEngine GestureEngine { get; set; }

        /// <summary>
        /// Gets a value indicating whether Kinect sensor is initialized
        /// </summary>
        public bool IsInitialized
        {
            get
            {
                return this.kinectSensor != null;
            }
        }

        /// <summary>
        /// Gets a value indicating whether initialized Kinect is running
        /// </summary>
        public bool IsRunning
        {
            get
            {
                return (this.kinectSensor != null) && this.kinectSensor.IsRunning;
            }
        }

        /// <summary>
        /// Gets a value indicating whether sensor is detecting a skeleton
        /// </summary>
        public bool IsTracking
        {
            get
            {
                return this.IsRunning && this.skeletons.Any(s => s.TrackingState == SkeletonTrackingState.Tracked);
            }
        }

        /// <summary>
        /// Gets or sets Kinect Sensor instance
        /// </summary>
        public KinectSensor KinectSensor
        {
            get
            {
                return this.kinectSensor;
            }

            set
            {
                // Initialize KinectSensor by setting new kinect sensor to manager,
                // this way ensures we correctly clean sensor data
                if (this.kinectSensor != value)
                {
                    this.InitializeSensor(value);
                }
            }
        }

        /// <summary>
        /// Gets status of initialized Kinect sensor
        /// </summary>
        public KinectStatus Status
        {
            get
            {
                if (this.kinectSensor == null)
                {
                    throw new NullReferenceException("Kinect sensor must be initialized before you can read status.");
                }

                return this.kinectSensor.Status;
            }
        }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// Closes currently active Kinect sensor and disposes all resources
        /// </summary>
        public void CloseKinect()
        {
            Logger.Info("Method: CloseKinect");

            if (this.kinectSensor != null)
            {
                Logger.Debug("Method: CloseKinect - Dispose");

                this.kinectSensor.ColorFrameReady -= this.KinectRuntimeColorFrameReady;
                this.kinectSensor.DepthFrameReady -= this.KinectRuntimeDepthFrameReady;
                this.kinectSensor.SkeletonFrameReady -= this.KinectRuntimeSkeletonFrameReady;

                if (this.kinectSensor.IsRunning)
                {
                    this.kinectSensor.Stop();
                }

                this.kinectSensor = null;

                this.KinectSensorActiveClosed(this, new KinectSensorEventArgs());
            }
        }

        /// <summary>
        /// The dispose.
        /// </summary>
        public void Dispose()
        {
            this.CloseKinect();

            // Dispose of all Kinect sensors
            foreach (KinectSensor kinect in KinectSensor.KinectSensors)
            {
                kinect.Dispose();
            }
        }

        /// <summary>
        /// Initializes default Kinect Sensor (first available)
        /// </summary>
        public void InitializeDefaultSensor()
        {
            // Close Kinect sensor if it is running
            if (this.kinectSensor != null)
            {
                this.CloseKinect();
            }

            // Loop through available Kinect sensors and choose first one
            foreach (KinectSensor kinect in KinectSensor.KinectSensors)
            {
                if ((kinect.Status != KinectStatus.Connected) || kinect.IsRunning)
                {
                    continue;
                }

                try
                {
                    this.InitializeSensor(kinect);

                    break;
                }
                catch (Exception ex)
                {
                    Logger.WarnException("Method: InitializeDefaultSensor - sensor failed", ex);
                }
            }
        }

        /// <summary>
        /// Initializes Kinect sensor.
        /// </summary>
        /// <param name="sensor">
        /// Sensor to initialize.
        /// </param>
        public void InitializeSensor(KinectSensor sensor)
        {
            // If we already have a sensor
            if (this.kinectSensor != null)
            {
                this.CloseKinect();
            }

            // Kinect Error Checks
            if (sensor == null)
            {
                throw new ArgumentNullException("sensor");
            }

            if (sensor.Status != KinectStatus.Connected)
            {
                Logger.Debug("Method: InitializeSensor - InvalidStatus: {0}", sensor.Status.ToString());

                throw new ArgumentException(
                    "Cannot initialize Kinect sensor, invalid status: " + sensor.Status.ToString());
            }

            if (sensor.IsRunning)
            {
                Logger.Debug("Method: InitializeSensor - IsRunning: {0}", sensor.IsRunning);

                throw new ArgumentException("Cannot initialize Kinect sensor, it is already in use.");
            }

            this.kinectSensor = sensor;
            this.nuiCamera = new BindableNUICamera(this.kinectSensor);

            // Setup streams
            this.kinectSensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);

            this.kinectSensor.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);

            this.kinectSensor.SkeletonStream.Enable();

            // Setup events
            this.kinectSensor.ColorFrameReady += this.KinectRuntimeColorFrameReady;
            this.kinectSensor.DepthFrameReady += this.KinectRuntimeDepthFrameReady;
            this.kinectSensor.SkeletonFrameReady += this.KinectRuntimeSkeletonFrameReady;

            this.KinectSensorActiveChanged(this, new KinectSensorEventArgs { Sensor = this.kinectSensor });
        }

        /// <summary>
        /// Starts current Kinect sensor
        /// </summary>
        public void StartKinect()
        {
            Logger.Info("Method: StartKinect");

            if (this.kinectSensor == null)
            {
                Logger.WarnException(
                    "Method: StartKinect - NotInitialized", 
                    new NullReferenceException("Kinect sensor is not yet initialized."));

                throw new NullReferenceException("Kinect sensor is not yet initialized.");
            }

            this.kinectSensor.Start();

            this.KinectSensorActiveStarted(this, new KinectSensorEventArgs { Sensor = this.kinectSensor });
        }

        #endregion

        #region Methods

        /// <summary>
        /// Wraps current Kinect runtime and sends manager events for depth frames
        /// </summary>
        /// <param name="sender">
        /// Sender of color frame ready event
        /// </param>
        /// <param name="e">
        /// Event arguments
        /// </param>
        private void KinectRuntimeColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            this.ColorFrameReady(sender, e);
        }

        /// <summary>
        /// Wraps current Kinect runtime and sends manager events for depth frames
        /// </summary>
        /// <param name="sender">
        /// Sender of depth frame ready event
        /// </param>
        /// <param name="e">
        /// Event arguments
        /// </param>
        private void KinectRuntimeDepthFrameReady(object sender, DepthImageFrameReadyEventArgs e)
        {
            this.DepthFrameReady(sender, e);
        }

        /// <summary>
        /// Function receives kinect skeleton frame, which contains data of currently tracked
        /// </summary>
        /// <param name="sender">
        /// Sender of skeleton frame ready event
        /// </param>
        /// <param name="e">
        /// Event arguments
        /// </param>
        private void KinectRuntimeSkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        {
            this.SkeletonFrameReady(sender, e);

            using (SkeletonFrame frame = e.OpenSkeletonFrame())
            {
                if ((frame == null) || (this.GestureController == null))
                {
                    return;
                }

                this.GestureController.Recognize(frame);

                frame.GetSkeletons(ref this.skeletons);

                // We search a tracking presenter, so we check if he is the only one and give him control
                if (this.skeletons.Any(s => s.TrackingState == SkeletonTrackingState.Tracked))
                {
                    // If we are tracking at least one skeleton, we check how many are there
                    int count = 0;

                    Skeleton lastTrackedPresenter = null;

                    foreach (Skeleton skeleton in this.skeletons)
                    {
                        if (skeleton.TrackingState == SkeletonTrackingState.Tracked)
                        {
                            lastTrackedPresenter = skeleton;

                            count++;
                        }
                    }

                    // Give control to presenter if he is the only tracked presenter
                    if ((count == 1) && (lastTrackedPresenter != null)
                        && (!this.GestureEngine.IsInControl(lastTrackedPresenter.TrackingId)))
                    {
                        Logger.Info(
                            "Event: SkeletonFrameReady - SetControl, TrackingId: {0}", lastTrackedPresenter.TrackingId);

                        this.GestureEngine.SetControl(lastTrackedPresenter.TrackingId);
                    }
                }
            }
        }

        /// <summary>
        /// Handles disconnection of the currently active kinect
        /// </summary>
        /// <param name="sender">
        /// Sender of on sensor status changed event
        /// </param>
        /// <param name="statusChangedEventArgs">
        /// Event arguments
        /// </param>
        private void KinectSensorsOnStatusChanged(object sender, StatusChangedEventArgs statusChangedEventArgs)
        {
            if (statusChangedEventArgs.Sensor == this.kinectSensor)
            {
                this.CloseKinect();
            }
        }

        /// <summary>
        /// Wraps current Kinect gesture recognition and sends manager events
        /// </summary>
        /// <param name="sender">
        /// Sender of gesture recognized event
        /// </param>
        /// <param name="e">
        /// Event arguments
        /// </param>
        private void OnGestureRecognized(object sender, GestureEventArgs e)
        {
            this.GestureRecognized(sender, e);
        }

        #endregion
    }
}